using System;
using System.IO;
using System.Threading;
using System.Xml;

namespace MimeCloud.Business.Helpers
{
    /// <summary>
    /// Contains a set of helper functions relevant to IO operations
    /// </summary>
    /// <remarks>Based on code donated by Alex Norcliffe</remarks>
    public class IO
    {
        /// <summary>
        /// The file extension used for generating serialization filenames
        /// </summary>
        public const string SerializationFileNameExtension = ".objectGraph";

        /// <summary>
        /// Gets the string contents of a file
        /// </summary>
        /// <param name="path">Absolute path to the file</param>
        /// <returns>string contents of the file at the given <see cref="P:path"/>, or string.Empty if an error occurrs</returns>
        public static string GetFileContents(string path)
        {
            string fileContents = string.Empty;
            if (File.Exists(path))
            {
                StreamReader sr = null;
                try
                {
                    FileStream fs = new FileStream(path, FileMode.Open, FileAccess.Read);
                    sr = new StreamReader(fs);
                    fileContents = sr.ReadToEnd();
                }
                catch { }
                finally
                {
                    if (!(sr == null))
                        sr.Close();
                }
            }
            return fileContents;
        }

        /// <summary>
        /// Recursively copies files and directories from one path to another
        /// </summary>
        /// <param name="sourcePath">Source path to copy from</param>
        /// <param name="destPath">Destination path to place the copy</param>
        /// <param name="overwrite">Whether or not to overwrite existing files at the destination</param>
        /// <exception cref="T:System.IO.DirectoryNotFoundException"></exception>
        public static void CopyDirectory(string sourcePath, string destPath, bool overwrite)
        {
            DirectoryInfo sourceDir = new DirectoryInfo(sourcePath);
            DirectoryInfo destDir = new DirectoryInfo(destPath);
            if ((sourceDir.Exists))
            {
                if (!((destDir.Exists)))
                    destDir.Create();
                foreach (FileInfo file in sourceDir.GetFiles())
                {
                    if ((overwrite))
                        file.CopyTo(Path.Combine(destDir.FullName, file.Name), true);
                    else
                    {
                        if (((File.Exists(Path.Combine(destDir.FullName, file.Name))) == false))
                            file.CopyTo(Path.Combine(destDir.FullName, file.Name), false);
                    }
                }
                foreach (DirectoryInfo dir in sourceDir.GetDirectories())
                    CopyDirectory(dir.FullName, Path.Combine(destDir.FullName, dir.Name), overwrite);
            }
            else
                throw new DirectoryNotFoundException("Could not find source path");
        }

        /// <summary>
        /// Deletes the files (potentially recursively) from given folder.
        /// </summary>
        public static bool DeleteFilesFromFolder(string path, string searchFilter, bool deleteNewerItems, DateTime compareDate, bool recurseSubFolders, bool removeEmptyFolders, bool leaveEmptyRootFolder)
        {
            bool returnSuccess = true;
            DirectoryInfo folder = new DirectoryInfo(path);
            if (folder.Name != ".svn")
            {
                foreach (FileInfo file in folder.GetFiles(searchFilter))
                {
                    if (!deleteNewerItems
                            ? file.LastWriteTime <
                              compareDate
                            : file.LastWriteTime > compareDate)
                    {
                        try
                        {
                            file.Delete();
                        }
                        catch (Exception ex)
                        {
                            returnSuccess = false;
                        }
                    }
                }

                if (recurseSubFolders)
                {
                    foreach (DirectoryInfo subFolder in folder.GetDirectories())
                    {
                        returnSuccess = DeleteFilesFromFolder(subFolder.FullName, searchFilter, deleteNewerItems,
                                                              compareDate, recurseSubFolders, removeEmptyFolders,
                                                              leaveEmptyRootFolder);
                    }
                }

                if (removeEmptyFolders)
                {
                    if (folder.FullName != path || !leaveEmptyRootFolder)
                    {
                        if (folder.GetFiles().Length == 0 &&
                            folder.GetDirectories().Length == 0)
                        {
                            try
                            {
                                folder.Delete(true);
                            }
                            catch (Exception ex)
                            {
                                returnSuccess = false;
                            }
                        }
                    }
                    else
                    {
                        /* Ignore */
                    }
                }
            }
            return returnSuccess;
        }

        /// <summary>
        /// Generates a valid filename from the input.
        /// </summary>
        /// <remarks>
        /// This method converts the inputted filename into a string which will not contain any of the characters
        /// specified in <see cref="Path.GetInvalidFileNameChars"/>. All invalid characters are replaced with an underscore.
        /// </remarks>
        /// <param name="fileName"></param>
        /// <returns></returns>
        public static string GetSafeFileName(string fileName)
        {
            #region Validate Parameters

            if (string.IsNullOrEmpty(fileName))
                throw new ArgumentNullException("fileName");

            #endregion

            foreach (char invalidChar in Path.GetInvalidFileNameChars())
                fileName = fileName.Replace(invalidChar, '_');
            return fileName.Replace('.', '_');
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="rootDirPath"></param>
        /// <param name="fileNameSeed"></param>
        /// <returns></returns>
        public static string GetSafePathForSerialization(string rootDirPath, string fileNameSeed)
        {
            #region Validate Parameters

            if (string.IsNullOrEmpty(rootDirPath))
                throw new ArgumentNullException("rootDirPath");

            if (string.IsNullOrEmpty(fileNameSeed))
                throw new ArgumentNullException("fileNameSeed");

            #endregion

            return Path.Combine(rootDirPath, GetSafeFilenameForSerialization(fileNameSeed));
        }

        /// <summary>
        /// Generates a valid filename for serializing an object, combining the <see cref="Environment.MachineName"/>
        /// with the provided filename and the <see cref="SerializationFileNameExtension"/>.
        /// </summary>
        /// <remarks>
        /// The resulting filename is in the format <value>MachineName_FileName.Extension</value> where FileName is the value
        /// of <paramref name="fileNameSeed"/>.
        /// </remarks>
        /// <param name="fileNameSeed"></param>
        /// <returns></returns>
        public static string GetSafeFilenameForSerialization(string fileNameSeed)
        {
            #region Validate Parameters

            if (string.IsNullOrEmpty(fileNameSeed))
                throw new ArgumentNullException("fileNameSeed");

            #endregion

            // Generate a filename for the serialized version of the cache item,
            // prefixing it with the machine name so that we can identify the source
            // of the serialized file without having to incur a deserialization hit
            return
                string.Format("{0}_{1}{2}", GetSafeFileName(Environment.MachineName), GetSafeFileName(fileNameSeed),
                              SerializationFileNameExtension);
        }

        /// <summary>
        /// Establishes whether the given filename could have been generated by <see cref="GetSafeFilenameForSerialization"/>
        /// on the current machine.
        /// </summary>
        /// <remarks>The check performed is whether <paramref name="path"/> starts with the current machine name.</remarks>
        /// <seealso cref="GetSafeFilenameForSerialization"/>
        /// <param name="path"></param>
        /// <returns></returns>
        public static bool IsSerializedPathFromThisMachine(string path)
        {
            #region Validate Parameters

            if (string.IsNullOrEmpty(path))
                throw new ArgumentNullException("path");

            #endregion

            path = Path.GetFileName(path);
            if (path.ToLower().StartsWith(GetSafeFileName(Environment.MachineName).ToLower()))
                return true;
            else
                return false;
        }

        /// <summary>
        /// Returns true if the specified file has completed writing and is ready for
        /// processing.
        /// </summary>
        /// <remarks>Internally, this method tries to open exclusive access to the file momentarily. Do not use this method
        /// in a thread-sensitive environment since another process could lock the file between this function returning
        /// <value>false</value> and subsquent code opening its own lock.</remarks>
        /// <returns><value>true</value> if the file cannot be opened, <value>false</value> if it available for reading.</returns>
        public static bool IsFileUnavailable(string filename)
        {
            //
            // If the file can be opened for exclusive access it means that the file
            // is no longer locked by another program and has completed writing.
            //
            try
            {
                using (FileStream inputStream = File.Open(filename, FileMode.Open, FileAccess.Read, FileShare.None))
                    return false;
            }
            catch
            {
                return true;
            }
        }

        /// <summary>
        /// Calls <see cref="WaitUntilFileAvailable(string,TimeSpan,TimeSpan)"/> with a poll period
        /// of 250 milliseconds and a maximum time limit of 5 seconds.
        /// </summary>
        /// <param name="absoluteFilePath"></param>
        /// <remarks>Internally, this method tries to open exclusive access to the file momentarily. Do not use this method
        /// in a thread-sensitive environment since another process could lock the file between this function returning
        /// <value>false</value> and subsquent code opening its own lock.</remarks>
        public static void WaitUntilFileAvailable(string absoluteFilePath)
        {
            WaitUntilFileAvailable(absoluteFilePath, TimeSpan.FromMilliseconds(250), TimeSpan.FromSeconds(5));
        }

        /// <summary>
        /// Polls a file until it becomes available for reading.
        /// </summary>
        /// <param name="absoluteFilePath"></param>
        /// <param name="pollPeriod">The amount of time the thread should sleep between polls.</param>
        /// <param name="waitTimeLimit">The maximum amount of time the method should try and gain access to the file.</param>
        /// <remarks>Internally, this method tries to open exclusive access to the file momentarily. Do not use this method
        /// in a thread-sensitive environment since another process could lock the file between this function returning
        /// <value>false</value> and subsquent code opening its own lock.</remarks>
        public static void WaitUntilFileAvailable(string absoluteFilePath, TimeSpan pollPeriod, TimeSpan waitTimeLimit)
        {
            DateTime receivedAt = DateTime.Now;

            //
            // The event will be raised as soon as the first byte is written to the file.
            // Check to see if the file has completed uploading. If it hasn't, wait for a
            // specified delay and check again.
            //
            while (true)
            {
                if (!IsFileUnavailable(absoluteFilePath))
                    return;
                else
                {
                    TimeSpan timeElapsed = DateTime.Now - receivedAt;

                    if (timeElapsed > waitTimeLimit)
                        break;
                    else
                        Thread.Sleep(pollPeriod);
                }
            }
            throw new IOException("Exceeded limit whilst waiting for file lock to be released.");
        }

        /// <summary>
        /// Tries to open a file handle and return a <see cref="FileStream"/>. Should the file be locked, waits until the file is available or a time limit is reached.
        /// </summary>
        /// <param name="absoluteFilePath">The file to open</param>
        /// <param name="fileMode">A <see cref="System.IO.FileMode"/> value that specifies whether a file is created if one does not exist, and determines whether the contents of existing files are retained or overwritten.</param>
        /// <param name="fileAccess">A <see cref="System.IO.FileAccess"/> value that specifies the operations that can be performed on the file.</param>
        /// <param name="fileShare">A <see cref="System.IO.FileShare"/> value specifying the type of access other threads have to the file.</param>
        /// <param name="waitTimeLimit">A <see cref="TimeSpan"/> value specifying the maximum amount of time to wait for the file to be available.</param>
        /// <returns></returns>
        public static FileStream OpenFileOrWait(string absoluteFilePath, FileMode fileMode, FileAccess fileAccess,
                                                FileShare fileShare, TimeSpan waitTimeLimit)
        {
            return OpenFileOrWait(absoluteFilePath, fileMode, fileAccess, fileShare, waitTimeLimit, 500);
        }

        /// <summary>
        /// Tries to open a file handle and return a <see cref="FileStream"/>. Should the file be locked, waits until the file is available or a time limit is reached.
        /// </summary>
        /// <param name="absoluteFilePath">The file to open</param>
        /// <param name="fileMode">A <see cref="System.IO.FileMode"/> value that specifies whether a file is created if one does not exist, and determines whether the contents of existing files are retained or overwritten.</param>
        /// <param name="fileAccess">A <see cref="System.IO.FileAccess"/> value that specifies the operations that can be performed on the file.</param>
        /// <param name="fileShare">A <see cref="System.IO.FileShare"/> value specifying the type of access other threads have to the file.</param>
        /// <param name="waitTimeLimit">A <see cref="TimeSpan"/> value specifying the maximum amount of time to wait for the file to be available.</param>
        /// <returns></returns>
        /// <param name="pollMilliseconds">number of milliseconds to wait before each attempt to open the file</param>
        public static FileStream OpenFileOrWait(string absoluteFilePath, FileMode fileMode, FileAccess fileAccess,
                                                FileShare fileShare, TimeSpan waitTimeLimit, int pollMilliseconds)
        {
            DateTime timeStart = DateTime.Now;

            while (true)
            {
                try
                {
                    return File.Open(absoluteFilePath, fileMode, fileAccess, fileShare);
                }
                catch
                {
                    TimeSpan timeElapsed = DateTime.Now - timeStart;

                    if (timeElapsed > waitTimeLimit)
                        break;
                    else
                        Thread.Sleep(pollMilliseconds);
                }
            }

            throw new IOException(
                string.Format(
                    "OpenFileOrWait: Exceeded time limit ({0}s) whilst waiting for file lock to be released on path {1}.",
                    waitTimeLimit.TotalSeconds, absoluteFilePath));
        }

        public static XmlDocument ReadXmlFileOrWait(string absoluteFilePath, TimeSpan waitTimeLimit,
                                                    int pollCountMilliseconds)
        {
            //get file stream
            DateTime timeStart = DateTime.Now;

            while (true)
            {
                try
                {
                    string result;
                    using (FileStream stream =
                        OpenFileOrWait(absoluteFilePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite,
                                       waitTimeLimit,
                                       pollCountMilliseconds))
                    {
                        StreamReader read = new StreamReader(stream);
                        result = read.ReadToEnd();
                    }
                    if (!string.IsNullOrEmpty(result))
                    {
                        XmlDocument resultDoc = new XmlDocument();
                        resultDoc.LoadXml(result);
                        return resultDoc;
                    }
                    else if ((DateTime.Now - timeStart) > waitTimeLimit)
                        break;
                    else
                        Thread.Sleep(pollCountMilliseconds);

                }
                catch
                {
                    if ((DateTime.Now - timeStart) > waitTimeLimit)
                        break;
                    else
                        Thread.Sleep(pollCountMilliseconds);
                }
            }

            throw new IOException(
                string.Format(
                    "ReadFileOrWait: Exceeded time limit ({0}s) whilst waiting for file lock to be released on path {1} or file to contain valid data.",
                    waitTimeLimit.TotalSeconds, absoluteFilePath));
        }
    }
}